package main

import (
	"fmt"
	"io/ioutil"
	"log"
	"os"
	"os/exec"
	"path"
	"path/filepath"
	"strconv"
	"strings"
	"time"

	"github.com/siddontang/go/ioutil2"
	"github.com/urfave/cli"
)

func main() {
	app := cli.NewApp()
	app.Name = "liverpcgen"
	app.Usage = "Genproto generate files to call liverpc, include *.pb.go and *.liverpc.go\n" +
		"EXAMPLE:\n   liverpcgen APP_NAME"
	app.Version = "1.0.0"
	app.Commands = []cli.Command{
		{
			Name:   "update",
			Usage:  "update the tool its self",
			Action: actionUpdate,
		},
	}
	app.Action = actionGenerate

	err := app.Run(os.Args)
	if err != nil {
		log.Fatal(err)
	}
}

func autoUpdate(ctx *cli.Context) (err error) {
	tfile, e := ioutil.ReadFile("/tmp/liverpcgentime")
	if e != nil {
		tfile = []byte{'0'}
	}
	ts, _ := strconv.ParseInt(string(tfile), 0, 64)
	current := time.Now().Unix()
	if (current - int64(ts)) > 3600*12 { // 12 hours old
		ioutil.WriteFile("/tmp/liverpcgentime", []byte(strconv.FormatInt(current, 10)), 0777)
		err = actionUpdate(ctx)
		if err != nil {
			return
		}
	}
	return
}

// actionGenerate invokes protoc to generate files
func actionGenerate(ctx *cli.Context) (err error) {
	if err = autoUpdate(ctx); err != nil {
		return
	}
	appName := ctx.Args().Get(0)
	if appName == "" {
		cli.ShowAppHelpAndExit(ctx, 1)
	}
	here := ctx.Args().Get(1)
	goPath := initGopath()
	goCommonPath := goPath + "/src/go-common"
	if !fileExist(goCommonPath) {
		return cli.NewExitError("go-common not exist : "+goCommonPath, 1)
	}
	var rpcPath = goCommonPath + "/app/service/live/" + appName + "/api/liverpc"
	if here == "here" {
		rpcPath, err = os.Getwd()
		if err != nil {
			return cli.NewExitError(err, 1)
		}
	}
	if !fileExist(rpcPath) {
		os.MkdirAll(rpcPath, 0755)
	}
	log.Println("Generate for path: " + rpcPath)
	// find protos
	hasProto := fileExistWithSuffix(rpcPath, ".proto")
	if !hasProto {
		return cli.NewExitError("No proto files found in path : "+rpcPath, 1)
	}
	files, err := ioutil.ReadDir(rpcPath)
	if err != nil {
		return cli.NewExitError("Cannot read dir : "+rpcPath+" error: "+err.Error(), 1)
	}
	for _, file := range files {
		if file.IsDir() {
			if strings.HasPrefix(file.Name(), "v") {
				//if !file
				p := rpcPath + "/" + file.Name()
				if fileExistWithSuffix(p, ".proto") {
					err = runCmd(fmt.Sprintf("cd %s && protoc -I%s -I%s -I. --gogofaster_out=. --liverpc_out=. %s/*.proto",
						rpcPath, goCommonPath+"/vendor", goCommonPath, file.Name()))
					if err != nil {
						return cli.NewExitError("run protoc err: "+err.Error(), 1)
					}
					log.Println("generated for path: ", p)
				} else {
					log.Println("no protofiles found in " + p + " skip")
				}
			}
		}
	}

	// generate client code
	log.Println("generating client.go ...")
	resetPrint()
	var outputCliDeclares []string
	var outputCliAssigns []string
	var outputImports []string

	split := strings.Split(rpcPath, "/")
	clientPkg := split[len(split)-1]
	ap("// Code generated by liverpcgen, DO NOT EDIT.")
	ap("// source: *.proto files under this directory")
	ap("// If you want to change this file, Please see README in go-common/app/tool/liverpc/protoc-gen-liverpc/")
	ap("package " + clientPkg)
	ap("")
	ap("import (")
	ap(`	"go-common/library/net/rpc/liverpc"`)
	pkgPrefix := strings.Replace(rpcPath, goPath+"/src/", "", 1)

	files, err = ioutil.ReadDir(rpcPath)
	if err != nil {
		return cli.NewExitError("Cannot read dir : "+rpcPath+" error: "+err.Error(), 1)
	}
	for _, file := range files {
		if strings.HasPrefix(file.Name(), "client.v") {
			pkg := strings.Split(file.Name(), ".")[1]
			pkgUpper := strings.ToUpper(pkg)
			var b []byte
			b, err = ioutil.ReadFile(rpcPath + "/" + file.Name())
			fileContent := string(b)
			if err != nil {
				return cli.NewExitError("fail to read file: "+rpcPath+"/"+file.Name(), 1)
			}
			fileContent = strings.TrimSuffix(fileContent, "\n")
			if fileContent != "" {
				names := strings.Split(fileContent, "\n")
				for _, name := range names {
					apVar(&outputCliDeclares, fmt.Sprintf("	// %s%s presents the controller in liverpc",
						pkgUpper, name))
					apVar(&outputCliDeclares, fmt.Sprintf("	%s%s %s.%sRPCClient", pkgUpper, name, pkg, name))
					apVar(&outputCliAssigns, fmt.Sprintf("	cli.%s%s = %s.New%sRPCClient(realCli)", pkgUpper, name, pkg, name))
				}
				log.Println("merge " + rpcPath + "/" + file.Name())
				apVar(&outputImports, fmt.Sprintf(`	"%s/%s"`, pkgPrefix, pkg))
			}
			os.Remove(rpcPath + "/" + file.Name())
		}
	}
	ap(outputImports...)
	ap(")")
	ap("")
	ap("// Client that represents a liverpc " + appName + " service api")
	ap("type Client struct {")
	ap("	cli *liverpc.Client")
	ap(outputCliDeclares...)
	ap("}")
	ap("")
	discoveryId := strings.Replace(appName, "-", "", -1)
	discoveryId = strings.Replace(discoveryId, "_", "", -1)
	discoveryId = "live." + discoveryId
	ap("// DiscoveryAppId the discovery id is not the tree name")
	ap(`var DiscoveryAppId = "` + discoveryId + `"`)
	ap("")
	ap("// New a Client that represents a liverpc " + discoveryId + " service api")
	ap("// conf can be empty, and it will use discovery to find service by default")
	ap("// conf.AppID will be overwrite by a fixed value DiscoveryAppId")
	ap("// therefore is no need to set")
	ap("func New(conf *liverpc.ClientConfig) *Client {")
	ap("	if conf == nil {")
	ap("		conf = &liverpc.ClientConfig{}")
	ap("	}")
	ap("	conf.AppID = DiscoveryAppId")
	ap("	var realCli = liverpc.NewClient(conf)")
	ap("	cli := &Client{cli:realCli}")
	ap("	cli.clientInit(realCli)")
	ap("	return cli")
	ap("}")
	ap("")

	ap("")
	ap("func (cli *Client)GetRawCli() *liverpc.Client {")
	ap("	return cli.cli")
	ap("}")
	ap("")

	ap("func (cli *Client)clientInit(realCli *liverpc.Client) {")
	ap(outputCliAssigns...)
	ap("}")
	output := getOutput()
	err = ioutil.WriteFile(rpcPath+"/client.go", []byte(output), 0755)
	if err != nil {
		return cli.NewExitError("write file err : "+err.Error()+"; path: "+rpcPath+"/client.go", 1)
	}
	runCmd("gofmt -s -w " + rpcPath + "/client.go")
	return
}

var buf []string

func resetPrint() {
	buf = make([]string, 0)
}

// append to default buf
func ap(args ...string) {
	buf = append(buf, args...)
}

// append to var "b"
func apVar(b *[]string, str ...string) {
	*b = append(*b, str...)
}

func getOutput() string {
	return strings.Join(buf, "\n")
}

// equals to `ls dir/*ext` has result
func fileExistWithSuffix(dir string, ext string) bool {
	hasFile := false
	filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
		if !info.IsDir() {
			if strings.HasSuffix(info.Name(), ext) {
				hasFile = true
				return fmt.Errorf("stop")
			}
		}
		return nil
	})
	return hasFile
}

// actionUpdate update the tools its self
func actionUpdate(ctx *cli.Context) (err error) {
	log.Printf("Updating liverpcgen.....")
	goPath := initGopath()
	goCommonPath := goPath + "/src/go-common"
	if !fileExist(goCommonPath) {
		return cli.NewExitError("go-common not exist : "+goCommonPath, 1)
	}
	cmd1 := fmt.Sprintf("cd %s/app/tool/liverpc/protoc-gen-liverpc && go install", goCommonPath)
	cmd2 := fmt.Sprintf("cd %s/app/tool/liverpc/liverpcgen && go install", goCommonPath)
	err = runCmd(cmd1)
	if err != nil {
		return cli.NewExitError("update fail: "+err.Error(), 1)
	}
	err = runCmd(cmd2)
	if err != nil {
		return cli.NewExitError("update fail: "+err.Error(), 1)
	}
	log.Printf("Updated!")
	return
}

// runCmd runs the cmd & print output (both stdout & stderr)
func runCmd(cmd string) (err error) {
	fmt.Printf("CMD: %s\n", cmd)
	out, err := exec.Command("/bin/bash", "-c", cmd).CombinedOutput()
	fmt.Print(string(out))
	return
}

func fileExist(file string) bool {
	return ioutil2.FileExists(file)
}

func initGopath() string {
	root, err := goPath()
	if err != nil || root == "" {
		log.Printf("can not read GOPATH, use ~/go as default GOPATH")
		root = path.Join(os.Getenv("HOME"), "go")
	}
	return root
}

func goPath() (string, error) {
	gopaths := strings.Split(os.Getenv("GOPATH"), ":")
	if len(gopaths) == 1 {
		return gopaths[0], nil
	}
	for _, gp := range gopaths {
		absgp, err := filepath.Abs(gp)
		if err != nil {
			return "", err
		}
		if fileExist(absgp + "/src/go-common") {
			return absgp, nil
		}
	}
	return "", fmt.Errorf("can't found current gopath")
}
